#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
Copyright (c) 2009 Timothe Faudot

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
"""

"""
This module is the main module of the application
It contains the OpenCV/GUI stuff and process the midi events
"""

#GUI
import cvpad
import wx

#midi
from rtmidi import RtMidiOut
from rtmidi import RtError

#others
import logging
import sys

#tune this according to the system capabilities, HIGHLY variable...
ID_TIMER_UPDATE = 1000

class OutputPanel(wx.Panel):
    """
    A panel showing the output to the user
    """
    def __init__(self, parent, id, size):
        wx.Panel.__init__(self, parent, id, size=size)
        self.bmp = wx.EmptyBitmap(size[0], size[1])
        
        self.Bind(wx.EVT_PAINT, self.onPaint)
        self.Bind(wx.EVT_LEFT_DOWN, self.onLeftDown)
        self.Bind(wx.EVT_LEFT_UP, self.onLeftUp)
        self.Bind(wx.EVT_MOTION, self.onMotion)
        
        self.startX = 0
        self.startY = 0
        self.currentX = -1
        self.currentY = -1
        
        self.locateX = -1
        self.locateY = -1
        
        self.selectingTemplate = False
        self.parent = parent
    
    def onLeftDown(self, evt):
        """Begin the tracking of the mouse for the selection of the template"""
        self.startX, self.startY = evt.GetPosition().x, evt.GetPosition().y
        self.parent.timerRefresh.Stop()
        evt.Skip()
        
    def onMotion(self, evt):
        """
        updates the tracking position of the mouse for the template selection
        """
        if not evt.Dragging() or not evt.LeftIsDown():
            return
        self.selectingTemplate = True
        self.currentX = evt.GetPosition().x
        self.currentY = evt.GetPosition().y
        self.Refresh()
        evt.Skip()
        
    def onLeftUp(self, evt):
        """
        Ends the tracking of the mouse for the template selection
        """
        if self.selectingTemplate:
            self.selectingTemplate = False
            self.parent.templateSelected(self.startX, self.startY,
                                         self.currentX, self.currentY)
            self.selectingTemplate = False
        self.parent.timerRefresh.Start(60)
        evt.Skip()
        
    def onPaint(self, evt):
        """
        Updates the output accordingly to the current state of the program
         - template selcted or not
         - video output on/off
        """
        dc=wx.BufferedPaintDC(self)
        dc.Clear()
        if self.bmp:
            dc.SelectObject(self.bmp)
            if self.selectingTemplate:
                dc.SetPen(wx.RED_PEN)
                dc.SetBrush(wx.TRANSPARENT_BRUSH)
                dc.DrawRectangle(self.startX, self.startY,
                                 self.currentX-self.startX,
                                 self.currentY-self.startY)
        if self.locateX != -1 and self.locateY != -1:
            dc.SetBrush(wx.RED_BRUSH)
            dc.SetPen(wx.RED_PEN)
            dc.DrawCircle(self.locateX, self.locateY, 3)
        evt.Skip()
    
class ControlPanel(wx.Panel):
    """
    Panel containing the commands available to the user
    """
    def __init__(self, parent, id):
        wx.Panel.__init__(self, parent, id)
        
        viewTpltBtn = wx.Button(self, -1, "View Template",
                                style=wx.NO_BORDER)
        self.Bind(wx.EVT_BUTTON, parent.onViewTemplate, viewTpltBtn)
        selectPortBtn = wx.Button(self, -1, "Select midi port",
                               style=wx.NO_BORDER)
        self.Bind(wx.EVT_BUTTON, parent.onSelectPort, selectPortBtn)
        sendMidiX = wx.Button(self, -1, "Send midi message x",
                               style=wx.NO_BORDER)
        self.Bind(wx.EVT_BUTTON, parent.onSendMidiX, sendMidiX)
        sendMidiY = wx.Button(self, -1, "Send midi message y",
                               style=wx.NO_BORDER)
        self.Bind(wx.EVT_BUTTON, parent.onSendMidiY, sendMidiY)
        goBtn = wx.ToggleButton(self, -1, "GO!",
                               style=wx.NO_BORDER)
        self.Bind(wx.EVT_TOGGLEBUTTON, parent.onGo, goBtn)
        chkVideo = wx.CheckBox(self, -1, "Show Video")
        chkVideo.SetValue(True)
        self.Bind(wx.EVT_CHECKBOX, parent.onShowHideVideo, chkVideo)
        
        sizer = wx.GridBagSizer(3,2)
        sizer.Add(viewTpltBtn, (0,0), flag=wx.EXPAND)
        sizer.Add(selectPortBtn, (0,1), flag=wx.EXPAND)
        sizer.Add(sendMidiX, (1,0), flag=wx.EXPAND)
        sizer.Add(sendMidiY, (1,1), flag=wx.EXPAND)
        sizer.Add(goBtn, pos=(2,0), flag=wx.EXPAND)
        sizer.Add(chkVideo, pos=(2,1), flag=wx.EXPAND)
        
        self.SetSizer(sizer)
        
class VisualMidiPad(wx.Frame):
    """
    Main frame of the application
    """
    def __init__(self, parent):
        wx.Frame.__init__(self, parent, -1,
            style=wx.MINIMIZE_BOX|wx.CLOSE_BOX|wx.CAPTION|wx.SYSTEM_MENU)
        
        self._initializeLog()
        
        self.cvPad = cvpad.CvPad()
        self.midiManager = RtMidiOut()
        self.templateIsSelected = False
        self.running = False
        self.showVideo = True
        
        self.outputPanel = OutputPanel(self, -1, (320,240))
        self.controlPanel = ControlPanel(self, -1)
        
        self.timerRefresh = wx.Timer(self, ID_TIMER_UPDATE)
        wx.EVT_TIMER(self, ID_TIMER_UPDATE, self.onNextFrame)
        self.Bind(wx.EVT_CLOSE, self.onClose)
        
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.outputPanel)
        sizer.Add(self.controlPanel)
        self.SetSizerAndFit(sizer)
        
        
        self.onSelectPort(None)
        self.timerRefresh.Start(60)
        self.Show(True)

    def _initializeLog(self):
        #DEBUG . INFO . WARN . ERROR . CRITICAL will be written on a file
        logging.basicConfig(level=logging.DEBUG,
                            format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s',
                            datefmt='%m-%d %H:%M',
                            filename='log.txt',
                            filemode='w')
        #WARN . ERROR . CRITICAL will be displayed on the console
        console = logging.StreamHandler()
        console.setLevel(logging.WARN)
        # set a format which is simpler for console use
        formatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s')
        # tell the handler to use this format
        console.setFormatter(formatter)
        # add the handler to the root logger
        logging.getLogger('').addHandler(console)
        
        self.logger = logging.getLogger(str(self.__class__))
    
    def onClose(self, evt):
        self.timerRefresh.Stop()
        self.midiManager.closePort()
        self.Destroy()
    
    def onViewTemplate(self, evt):
        self.cvPad.showTemplate()
        evt.Skip()
        
    def onSelectPort(self, evt):
        nbPorts = self.midiManager.getPortCount()
        self.logger.info("Nb ports detected: %d"%nbPorts)
        if nbPorts>0:
            portNames = []
            for i in xrange(nbPorts):
                portNames.append(self.midiManager.getPortName(i))
            dlg = wx.SingleChoiceDialog(
                    self, 'Please select the midi port you want to use',
                    'Midi ports', portNames, wx.CHOICEDLG_STYLE)
    
            if dlg.ShowModal() == wx.ID_OK:
                midiPort = dlg.GetSelection()
                try:
                    self.midiManager.closePort()
                except RtError, msg:
                    pass
                self.midiManager.openPort(midiPort)
                self.logger.info("selected port %d"%midiPort)
                
            dlg.Destroy()
        else:
            wx.MessageBox("No midi port available on your system,\
please install a virtual driver first.",
                          "Error", wx.ICON_ERROR | wx.OK)
            self.logger.critical("No available midi port detected")
            sys.exit(-1)
        if evt:
            evt.Skip()
        
    def templateSelected(self, x1, y1, x2, y2):
        """checks if the template selection is OK and if so, sets it"""
        if x1<x2 and y1<y2:
            self.cvPad.setTemplate(x1, y1, x2, y2)
        elif x1<x2 and y1>y2:
            self.cvPad.setTemplate(x1, y2, x2, y1)            
        elif x1>x2 and y1>y2:
            self.cvPad.setTemplate(x2, y2, x1, y1)
        elif x1>x2 and y1<y2:
            self.cvPad.setTemplate(x2, y1, x1, y2)
        self.templateIsSelected = True
        
    def onNextFrame(self, evt):
        """Timed event to refresh the camera output and send midi events"""
        self.cvPad.nextFrame()
        if self.showVideo:
            frame = self.cvPad.getThumb()
            self.outputPanel.bmp = wx.BitmapFromBuffer(frame.width, frame.height, frame.imageData)
        if self.templateIsSelected:
            x, y = self.cvPad.locateTemplate()
            #print x, y
            self.outputPanel.locateX = x
            self.outputPanel.locateY = y
            
            midiX = x*127/self.outputPanel.GetSize()[0]
            midiY = y*127/self.outputPanel.GetSize()[1]
            
            if self.running:
                #0xb0:Control change 0
                #0x07:Volume
                #0x02:Modulation wheel
                try:
                    self.midiManager.sendMessage(0xb0, 0x07, midiX)
                    self.midiManager.sendMessage(0xb0, 0x02, midiY)
                except RtError, msg:
                    self.logger.critical(msg)
                #print "sent x->%d, y->%d"%(x, y)

        self.outputPanel.Refresh()
        evt.Skip()
        
    def onSendMidiX(self, evt):
        """Sends a X event for usage with midi-learn"""
        self.midiManager.sendMessage(0xb0, 0x07, 0x01)
        self.logger.info("sent message as x axix would have done")
        evt.Skip()
        
    def onSendMidiY(self, evt):
        """Sends a Y event for usage with midi-learn"""
        self.midiManager.sendMessage(0xb0, 0x02, 0x01)
        self.logger.info("sent message as y axix would have done")
        evt.Skip()
        
    def onGo(self, evt):
        """Sets the sending of the midi events on/off"""
        self.running = not self.running
        self.logger.info("midi events are %s from now on"%("sent" if self.running else "inhibited"))
        evt.Skip()
        
    def onShowHideVideo(self, evt):
        """Shows or disable the video output"""
        self.showVideo = evt.Checked()
        if not self.showVideo:
            self.outputPanel.bmp = None
    
if __name__=="__main__":
    app = wx.App()
    app.RestoreStdio()
    VisualMidiPad(None)
    app.MainLoop()